/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package com.feilong.lib.javassist.compiler;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;

import com.feilong.lib.javassist.ClassPool;
import com.feilong.lib.javassist.CtClass;
import com.feilong.lib.javassist.CtField;
import com.feilong.lib.javassist.Modifier;
import com.feilong.lib.javassist.NotFoundException;
import com.feilong.lib.javassist.bytecode.AccessFlag;
import com.feilong.lib.javassist.bytecode.ClassFile;
import com.feilong.lib.javassist.bytecode.Descriptor;
import com.feilong.lib.javassist.bytecode.MethodInfo;
import com.feilong.lib.javassist.compiler.ast.ASTList;
import com.feilong.lib.javassist.compiler.ast.ASTree;
import com.feilong.lib.javassist.compiler.ast.Declarator;
import com.feilong.lib.javassist.compiler.ast.Keyword;
import com.feilong.lib.javassist.compiler.ast.Symbol;

/* Code generator methods depending on javassist.* classes.
 */
public class MemberResolver implements TokenId{

    private ClassPool classPool;

    public MemberResolver(ClassPool cp){
        classPool = cp;
    }

    public ClassPool getClassPool(){
        return classPool;
    }

    private static void fatal() throws CompileError{
        throw new CompileError("fatal");
    }

    public static class Method{

        public CtClass    declaring;

        public MethodInfo info;

        public int        notmatch;

        public Method(CtClass c, MethodInfo i, int n){
            declaring = c;
            info = i;
            notmatch = n;
        }

        /**
         * Returns true if the invoked method is static.
         */
        public boolean isStatic(){
            int acc = info.getAccessFlags();
            return (acc & AccessFlag.STATIC) != 0;
        }
    }

    public Method lookupMethod(
                    CtClass clazz,
                    CtClass currentClass,
                    MethodInfo current,
                    String methodName,
                    int[] argTypes,
                    int[] argDims,
                    String[] argClassNames) throws CompileError{
        Method maybe = null;
        // to enable the creation of a recursively called method
        if (current != null && clazz == currentClass){
            if (current.getName().equals(methodName)){
                int res = compareSignature(current.getDescriptor(), argTypes, argDims, argClassNames);
                if (res != NO){
                    Method r = new Method(clazz, current, res);
                    if (res == YES){
                        return r;
                    }
                    maybe = r;
                }
            }
        }

        Method m = lookupMethod(clazz, methodName, argTypes, argDims, argClassNames, maybe != null);
        if (m != null){
            return m;
        }
        return maybe;
    }

    private Method lookupMethod(CtClass clazz,String methodName,int[] argTypes,int[] argDims,String[] argClassNames,boolean onlyExact)
                    throws CompileError{
        Method maybe = null;
        ClassFile cf = clazz.getClassFile2();
        // If the class is an array type, the class file is null.
        // If so, search the super class java.lang.Object for clone() etc.
        if (cf != null){
            List<MethodInfo> list = cf.getMethods();
            for (MethodInfo minfo : list){
                if (minfo.getName().equals(methodName) && (minfo.getAccessFlags() & AccessFlag.BRIDGE) == 0){
                    int res = compareSignature(minfo.getDescriptor(), argTypes, argDims, argClassNames);
                    if (res != NO){
                        Method r = new Method(clazz, minfo, res);
                        if (res == YES){
                            return r;
                        }else if (maybe == null || maybe.notmatch > res){
                            maybe = r;
                        }
                    }
                }
            }
        }

        if (onlyExact){
            maybe = null;
        }else if (maybe != null){
            return maybe;
        }

        int mod = clazz.getModifiers();
        boolean isIntf = Modifier.isInterface(mod);
        try{
            // skip searching java.lang.Object if clazz is an interface type.
            if (!isIntf){
                CtClass pclazz = clazz.getSuperclass();
                if (pclazz != null){
                    Method r = lookupMethod(pclazz, methodName, argTypes, argDims, argClassNames, onlyExact);
                    if (r != null){
                        return r;
                    }
                }
            }
        }catch (NotFoundException e){}

        try{
            CtClass[] ifs = clazz.getInterfaces();
            for (CtClass intf : ifs){
                Method r = lookupMethod(intf, methodName, argTypes, argDims, argClassNames, onlyExact);
                if (r != null){
                    return r;
                }
            }

            if (isIntf){
                // finally search java.lang.Object.
                CtClass pclazz = clazz.getSuperclass();
                if (pclazz != null){
                    Method r = lookupMethod(pclazz, methodName, argTypes, argDims, argClassNames, onlyExact);
                    if (r != null){
                        return r;
                    }
                }
            }
        }catch (NotFoundException e){}

        return maybe;
    }

    private static final int YES = 0;

    private static final int NO  = -1;

    /*
     * Returns YES if actual parameter types matches the given signature.
     *
     * argTypes, argDims, and argClassNames represent actual parameters.
     *
     * This method does not correctly implement the Java method dispatch
     * algorithm.
     *
     * If some of the parameter types exactly match but others are subtypes of
     * the corresponding type in the signature, this method returns the number
     * of parameter types that do not exactly match.
     */
    private int compareSignature(String desc,int[] argTypes,int[] argDims,String[] argClassNames) throws CompileError{
        int result = YES;
        int i = 1;
        int nArgs = argTypes.length;
        if (nArgs != Descriptor.numOfParameters(desc)){
            return NO;
        }

        int len = desc.length();
        for (int n = 0; i < len; ++n){
            char c = desc.charAt(i++);
            if (c == ')'){
                return (n == nArgs ? result : NO);
            }else if (n >= nArgs){
                return NO;
            }

            int dim = 0;
            while (c == '['){
                ++dim;
                c = desc.charAt(i++);
            }

            if (argTypes[n] == NULL){
                if (dim == 0 && c != 'L'){
                    return NO;
                }

                if (c == 'L'){
                    i = desc.indexOf(';', i) + 1;
                }
            }else if (argDims[n] != dim){
                if (!(dim == 0 && c == 'L' && desc.startsWith("java/lang/Object;", i))){
                    return NO;
                }

                // if the thread reaches here, c must be 'L'.
                i = desc.indexOf(';', i) + 1;
                result++;
                if (i <= 0){
                    return NO; // invalid descriptor?
                }
            }else if (c == 'L'){ // not compare
                int j = desc.indexOf(';', i);
                if (j < 0 || argTypes[n] != CLASS){
                    return NO;
                }

                String cname = desc.substring(i, j);
                if (!cname.equals(argClassNames[n])){
                    CtClass clazz = lookupClassByJvmName(argClassNames[n]);
                    try{
                        if (clazz.subtypeOf(lookupClassByJvmName(cname))){
                            result++;
                        }else{
                            return NO;
                        }
                    }catch (NotFoundException e){
                        result++; // should be NO?
                    }
                }

                i = j + 1;
            }else{
                int t = descToType(c);
                int at = argTypes[n];
                if (t != at){
                    if (t == INT && (at == SHORT || at == BYTE || at == CHAR)){
                        result++;
                    }else{
                        return NO;
                    }
                }
            }
        }

        return NO;
    }

    /**
     * Only used by fieldAccess() in MemberCodeGen and TypeChecker.
     *
     * @param jvmClassName
     *            a JVM class name. e.g. java/lang/String
     * @see #lookupClass(String, boolean)
     */
    public CtField lookupFieldByJvmName2(String jvmClassName,Symbol fieldSym,ASTree expr) throws NoFieldException{
        String field = fieldSym.get();
        CtClass cc = null;
        try{
            cc = lookupClass(jvmToJavaName(jvmClassName), true);
        }catch (CompileError e){
            // EXPR might be part of a qualified class name.
            throw new NoFieldException(jvmClassName + "/" + field, expr);
        }

        try{
            return cc.getField(field);
        }catch (NotFoundException e){
            // maybe an inner class.
            jvmClassName = javaToJvmName(cc.getName());
            throw new NoFieldException(jvmClassName + "$" + field, expr);
        }
    }

    /**
     * @param jvmClassName
     *            a JVM class name. e.g. java/lang/String
     */
    public CtField lookupFieldByJvmName(String jvmClassName,Symbol fieldName) throws CompileError{
        return lookupField(jvmToJavaName(jvmClassName), fieldName);
    }

    /**
     * @param className
     *            a qualified class name. e.g. java.lang.String
     */
    public CtField lookupField(String className,Symbol fieldName) throws CompileError{
        CtClass cc = lookupClass(className, false);
        try{
            return cc.getField(fieldName.get());
        }catch (NotFoundException e){}
        throw new CompileError("no such field: " + fieldName.get());
    }

    public CtClass lookupClassByName(ASTList name) throws CompileError{
        return lookupClass(Declarator.astToClassName(name, '.'), false);
    }

    public CtClass lookupClassByJvmName(String jvmName) throws CompileError{
        return lookupClass(jvmToJavaName(jvmName), false);
    }

    public CtClass lookupClass(Declarator decl) throws CompileError{
        return lookupClass(decl.getType(), decl.getArrayDim(), decl.getClassName());
    }

    /**
     * @param classname
     *            jvm class name.
     */
    public CtClass lookupClass(int type,int dim,String classname) throws CompileError{
        String cname = "";
        CtClass clazz;
        if (type == CLASS){
            clazz = lookupClassByJvmName(classname);
            if (dim > 0){
                cname = clazz.getName();
            }else{
                return clazz;
            }
        }else{
            cname = getTypeName(type);
        }

        while (dim-- > 0){
            cname += "[]";
        }

        return lookupClass(cname, false);
    }

    /*
     * type cannot be CLASS
     */
    static String getTypeName(int type) throws CompileError{
        String cname = "";
        switch (type) {
            case BOOLEAN:
                cname = "boolean";
                break;
            case CHAR:
                cname = "char";
                break;
            case BYTE:
                cname = "byte";
                break;
            case SHORT:
                cname = "short";
                break;
            case INT:
                cname = "int";
                break;
            case LONG:
                cname = "long";
                break;
            case FLOAT:
                cname = "float";
                break;
            case DOUBLE:
                cname = "double";
                break;
            case VOID:
                cname = "void";
                break;
            default:
                fatal();
        }

        return cname;
    }

    /**
     * @param name
     *            a qualified class name. e.g. java.lang.String
     */
    public CtClass lookupClass(String name,boolean notCheckInner) throws CompileError{
        Map<String, String> cache = getInvalidNames();
        String found = cache.get(name);
        if (found == INVALID){
            throw new CompileError("no such class: " + name);
        }else if (found != null){
            try{
                return classPool.get(found);
            }catch (NotFoundException e){}
        }

        CtClass cc = null;
        try{
            cc = lookupClass0(name, notCheckInner);
        }catch (NotFoundException e){
            cc = searchImports(name);
        }

        cache.put(name, cc.getName());
        return cc;
    }

    private static final String                                   INVALID         = "<invalid>";

    private static Map<ClassPool, Reference<Map<String, String>>> invalidNamesMap = new WeakHashMap<>();

    private Map<String, String>                                   invalidNames    = null;

    // for unit tests
    public static int getInvalidMapSize(){
        return invalidNamesMap.size();
    }

    private Map<String, String> getInvalidNames(){
        Map<String, String> ht = invalidNames;
        if (ht == null){
            synchronized (MemberResolver.class){
                Reference<Map<String, String>> ref = invalidNamesMap.get(classPool);
                if (ref != null){
                    ht = ref.get();
                }

                if (ht == null){
                    ht = new Hashtable<>();
                    invalidNamesMap.put(classPool, new WeakReference<>(ht));
                }
            }

            invalidNames = ht;
        }

        return ht;
    }

    private CtClass searchImports(String orgName) throws CompileError{
        if (orgName.indexOf('.') < 0){
            Iterator<String> it = classPool.getImportedPackages();
            while (it.hasNext()){
                String pac = it.next();
                String fqName = pac.replaceAll("\\.$", "") + "." + orgName;
                try{
                    return classPool.get(fqName);
                }catch (NotFoundException e){
                    try{
                        if (pac.endsWith("." + orgName)){
                            return classPool.get(pac);
                        }
                    }catch (NotFoundException e2){}
                }
            }
        }

        getInvalidNames().put(orgName, INVALID);
        throw new CompileError("no such class: " + orgName);
    }

    private CtClass lookupClass0(String classname,boolean notCheckInner) throws NotFoundException{
        CtClass cc = null;
        do{
            try{
                cc = classPool.get(classname);
            }catch (NotFoundException e){
                int i = classname.lastIndexOf('.');
                if (notCheckInner || i < 0){
                    throw e;
                }
                StringBuffer sbuf = new StringBuffer(classname);
                sbuf.setCharAt(i, '$');
                classname = sbuf.toString();
            }
        }while (cc == null);
        return cc;
    }

    /*
     * Converts a class name into a JVM-internal representation.
     *
     * It may also expand a simple class name to java.lang.*.
     * For example, this converts Object into java/lang/Object.
     */
    public String resolveClassName(ASTList name) throws CompileError{
        if (name == null){
            return null;
        }
        return javaToJvmName(lookupClassByName(name).getName());
    }

    /*
     * Expands a simple class name to java.lang.*.
     * For example, this converts Object into java/lang/Object.
     */
    public String resolveJvmClassName(String jvmName) throws CompileError{
        if (jvmName == null){
            return null;
        }
        return javaToJvmName(lookupClassByJvmName(jvmName).getName());
    }

    public static CtClass getSuperclass(CtClass c) throws CompileError{
        try{
            CtClass sc = c.getSuperclass();
            if (sc != null){
                return sc;
            }
        }catch (NotFoundException e){}
        throw new CompileError("cannot find the super class of " + c.getName());
    }

    public static CtClass getSuperInterface(CtClass c,String interfaceName) throws CompileError{
        try{
            CtClass[] intfs = c.getInterfaces();
            for (CtClass intf : intfs){
                if (intf.getName().equals(interfaceName)){
                    return intf;
                }
            }
        }catch (NotFoundException e){}
        throw new CompileError("cannot find the super inetrface " + interfaceName + " of " + c.getName());
    }

    public static String javaToJvmName(String classname){
        return classname.replace('.', '/');
    }

    public static String jvmToJavaName(String classname){
        return classname.replace('/', '.');
    }

    public static int descToType(char c) throws CompileError{
        switch (c) {
            case 'Z':
                return BOOLEAN;
            case 'C':
                return CHAR;
            case 'B':
                return BYTE;
            case 'S':
                return SHORT;
            case 'I':
                return INT;
            case 'J':
                return LONG;
            case 'F':
                return FLOAT;
            case 'D':
                return DOUBLE;
            case 'V':
                return VOID;
            case 'L':
            case '[':
                return CLASS;
            default:
                fatal();
                return VOID; // never reach here
        }
    }

    public static int getModifiers(ASTList mods){
        int m = 0;
        while (mods != null){
            Keyword k = (Keyword) mods.head();
            mods = mods.tail();
            switch (k.get()) {
                case STATIC:
                    m |= Modifier.STATIC;
                    break;
                case FINAL:
                    m |= Modifier.FINAL;
                    break;
                case SYNCHRONIZED:
                    m |= Modifier.SYNCHRONIZED;
                    break;
                case ABSTRACT:
                    m |= Modifier.ABSTRACT;
                    break;
                case PUBLIC:
                    m |= Modifier.PUBLIC;
                    break;
                case PROTECTED:
                    m |= Modifier.PROTECTED;
                    break;
                case PRIVATE:
                    m |= Modifier.PRIVATE;
                    break;
                case VOLATILE:
                    m |= Modifier.VOLATILE;
                    break;
                case TRANSIENT:
                    m |= Modifier.TRANSIENT;
                    break;
                case STRICT:
                    m |= Modifier.STRICT;
                    break;
            }
        }

        return m;
    }
}
